﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Roslyn.Compilers.Common;
using Roslyn.Compilers.CSharp;
using CompleterLib.AbstractTypes;
using Util;
using CompleterLib.Util;

namespace CompleterLib
{
    public static class CompleterUtil
    {
        public const int MAX_COMPLETIONS = 50;

        public static IEnumerable<Completion> GetCompletions(this CommonSyntaxNode synNode,
            SemanticModel semModel,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            var exp = synNode as ExpressionSyntax;
            var completer = new Completer(semModel, exp.Span.Start, cancellationToken);
            //if (exp.IsPartial(semModel))
            return completer.GetCompletions(exp);
        }

        public static bool IsPartial(this CommonSyntaxNode synNode,
                                     SemanticModel semModel)
        {
            if (synNode is TypeSyntax)
            {
                var semInfo = semModel.GetSemanticInfo((TypeSyntax)synNode);
                if (semInfo.Type.IsValidType() && semInfo.Type.Equals(semInfo.Symbol))
                {
                    return true;
                }
            }
            if (!(synNode is ExpressionSyntax) || synNode is LiteralExpressionSyntax)
            {
                return false;
            }
            else if (synNode is InvocationExpressionSyntax)
            {
                var call = synNode as InvocationExpressionSyntax;
                var kind = call.GetInvocationExpressionKind(semModel);
                if (kind == InvocationExpressionKind.UNKNOWN
                    || kind == InvocationExpressionKind.UNKNOWN_STATIC)
                {
                    return true;
                }
                else if (kind == InvocationExpressionKind.ERROR)
                {
                    return false;
                }
                else
                {
                    return call.ArgumentList.Arguments
                        .Any(a => a.Expression.IsPartial(semModel));
                }
            }
            else if (synNode is ObjectCreationExpressionSyntax)
            {
                var create = synNode as ObjectCreationExpressionSyntax;
                return create.ArgumentList.Arguments.Any(a => a.Expression.IsPartial(semModel));
            }
            else if (synNode is MemberAccessExpressionSyntax)
            {
                var access = synNode as MemberAccessExpressionSyntax;
                var type = semModel.GetTypeInfo(access).Type;
                if (!(type == null || type is ErrorTypeSymbol))
                {
                    return false;
                }
                else
                {
                    return new string[] { "x", "xx", "f", "ff", "m",
                        "xUnknown", "xxUnknown", "fUnknown", "ffUnknown", "mUnknown"}
                        .Contains(access.Name.PlainName)
                        || access.Expression.IsPartial(semModel);
                }
            }
            else if (synNode is IdentifierNameSyntax)
            {
                var ident = synNode as IdentifierNameSyntax;
                var type = semModel.GetSemanticInfo(ident).Type;
                if (!(type == null || type is ErrorTypeSymbol))
                {
                    return false;
                }
                return new string[] { "xx", "xxUnknown" }.Contains(ident.PlainName);
            }
            else if (synNode is BinaryExpressionSyntax)
            {
                var bin = synNode as BinaryExpressionSyntax;
                return bin.Left.IsPartial(semModel) || bin.Right.IsPartial(semModel);
            }
            else if (synNode is SimpleLambdaExpressionSyntax
                || synNode is ParenthesizedLambdaExpressionSyntax)
            {
                return false;
            }
            else if (synNode is CastExpressionSyntax)
            {
                var cast = (CastExpressionSyntax)synNode;
                return cast.Expression.IsPartial(semModel);
            }
            else
            {
                // TODO are all expression types really handled right?
                /*throw new Exception("Unknown expression type in IsPartial(): "
                    + synNode.GetType());*/
                return false;
            }
        }

        public static bool RewritePartial(this CommonSyntaxNode synNode,
                                          SemanticModel semModel,
                                          out string withUnknown)
        {
            if (!(synNode is ExpressionSyntax) || synNode is LiteralExpressionSyntax)
            {
                withUnknown = synNode.GetText();
                return false;
            }
            else if (synNode is TypeSyntax)
            {
                var type = semModel.GetTypeInfo((TypeSyntax)synNode).Type;
                var sym = semModel.GetSymbolInfo((TypeSyntax)synNode).Symbol;
                if (type.IsValidType() && type.Equals(sym))
                {
                    withUnknown = "(" + synNode.GetText() + ")xxUnknown";
                    return true;
                }
            }
            if (synNode is InvocationExpressionSyntax)
            {
                var call = synNode as InvocationExpressionSyntax;
                string callExpWithUnknown;
                var kind = call.GetInvocationExpressionKind(semModel, out callExpWithUnknown);
                string[] argRewrites = new string[call.ArgumentList.Arguments.Count];
                bool argumentsPartial = call.ArgumentList.Arguments
                    .Select((a, i) => a.Expression.RewritePartial(semModel, out argRewrites[i]))
                    .ToList()
                    .Any();
                if (kind == InvocationExpressionKind.UNKNOWN
                    || kind == InvocationExpressionKind.UNKNOWN_STATIC)
                {
                    withUnknown = callExpWithUnknown
                        + "(" + argRewrites.CommaSeparated() + ")";
                    return true;
                }
                else if (kind == InvocationExpressionKind.ERROR)
                {
                    withUnknown = synNode.GetText();
                    return false;
                }
                else
                {
                    withUnknown = call.Expression.GetText()
                        + "(" + argRewrites.CommaSeparated() + ")";
                    return argumentsPartial;
                }
            }
            else if (synNode is ObjectCreationExpressionSyntax)
            {
                var create = synNode as ObjectCreationExpressionSyntax;
                string[] argRewrites = new string[create.ArgumentList.Arguments.Count];
                bool argumentsPartial = create.ArgumentList.Arguments
                    .Select((a, i) => a.Expression.RewritePartial(semModel, out argRewrites[i]))
                    .ToList()
                    .Any();
                // TODO Handle initializers?
                withUnknown = create.NewKeyword.GetText() + " " + create.Type + "("
                    + argRewrites.CommaSeparated() + ")" + create.Initializer;
                return argumentsPartial;
            }
            else if (synNode is MemberAccessExpressionSyntax)
            {
                var access = synNode as MemberAccessExpressionSyntax;
                var type = semModel.GetTypeInfo(access).Type;
                if (!(type == null || type is ErrorTypeSymbol))
                {
                    withUnknown = synNode.GetText();
                    return false;
                }
                else
                {
                    bool unknownLookup = new string[] { "x", "xx", "f", "ff", "m",
                        "xUnknown", "xxUnknown", "fUnknown", "ffUnknown", "mUnknown"}
                        .Contains(access.Name.PlainName);
                    string name = access.Name.PlainName;
                    if (unknownLookup && name.Length <= 2)
                    {
                        name += "Unknown";
                    }
                    if (name[0] == 'm')
                    {
                        name += "()";
                    }
                    string parent;
                    bool partialBase = access.Expression.RewritePartial(semModel, out parent);
                    withUnknown = parent + "." + name;
                    return unknownLookup || partialBase;
                }
            }
            else if (synNode is IdentifierNameSyntax)
            {
                var ident = synNode as IdentifierNameSyntax;
                var type = semModel.GetTypeInfo(ident).Type;
                if (!(type == null || type is ErrorTypeSymbol))
                {
                    withUnknown = synNode.GetText();
                    return false;
                }
                if (new string[] { "xx", "xxUnknown" }.Contains(ident.PlainName))
                {
                    withUnknown = "xxUnknown";// +string.Concat(ident.GetTrailingTrivia());
                    return true;
                }
                else
                {
                    withUnknown = synNode.GetText();
                    return false;
                }
            }
            else if (synNode is BinaryExpressionSyntax)
            {
                var bin = synNode as BinaryExpressionSyntax;
                string left, right;
                var res = bin.Left.RewritePartial(semModel, out left)
                    | bin.Right.RewritePartial(semModel, out right);
                withUnknown = left + " " + bin.OperatorToken.GetText() + " " + right;
                return res;
            }
            else if (synNode is ArrayCreationExpressionSyntax)
            {
                // TODO Support this somehow?
                withUnknown = synNode.GetText();
                return false;
            }
            else if (synNode is CastExpressionSyntax)
            {
                var cast = (CastExpressionSyntax)synNode;
                var res = cast.Expression.RewritePartial(semModel, out withUnknown);
                withUnknown = "(" + cast.Type + ") " + withUnknown;
                return res;
            }
            else
            {
                withUnknown = synNode.GetText();
                return false;
                /*throw new Exception("Unknown expression type in IsPartial(): "
                    + synNode.GetType());*/
            }
        }

        public static TypeSymbol GetRetType(this ExpressionSyntax synNode,
            SemanticModel semModel)
        {
            var semInfo = semModel.GetTypeInfo(synNode);
            var semType = semInfo.Type;
            if (semType != null && !(semType is ErrorTypeSymbol))
            {
                return semType;
            }
            var parent = synNode.Parent;
            if (parent is ExpressionStatementSyntax)
            {
                return semModel.Compilation.GetSpecialType(
                    Roslyn.Compilers.SpecialType.System_Void);
            }
            else if (parent is EqualsValueClauseSyntax
                && parent.Parent is VariableDeclaratorSyntax
                && parent.Parent.Parent is VariableDeclarationSyntax)
            {
                var decl = parent.Parent.Parent as VariableDeclarationSyntax;
                return semModel.GetTypeInfo(decl.Type).Type;
            }
            else if (parent is ReturnStatementSyntax)
            {
                // TODO yield?
                var ret = parent as ReturnStatementSyntax;
                var containingMethod = ret.FirstAncestorOrSelf<SyntaxNode>(node =>
                    node is MethodDeclarationSyntax) as MethodDeclarationSyntax;
                return semModel.GetTypeInfo(containingMethod.ReturnType).Type;
            }
            else if (parent is BinaryExpressionSyntax && parent.Kind.Equals(SyntaxKind.AssignExpression))
            {
                var bin = parent as BinaryExpressionSyntax;
                var leftType = semModel.GetTypeInfo(bin.Left).Type;
                if (synNode == bin.Right && leftType != null && !(leftType is ErrorTypeSymbol))
                {
                    return leftType;
                }
                else if (bin.Parent is ExpressionSyntax)
                {
                    return bin.GetRetType(semModel);
                }
                else
                {
                    return null;
                }
            }
            else if (parent is BinaryExpressionSyntax)
            {
                if (parent.Kind.Equals(SyntaxKind.LogicalAndExpression)
                    || parent.Kind.Equals(SyntaxKind.LogicalOrExpression))
                {
                    // TODO ... or has an operator overload defined...
                    return semModel.Compilation.GetSpecialType(
                        Roslyn.Compilers.SpecialType.System_Boolean);
                }
            }
            else if (parent is PrefixUnaryExpressionSyntax)
            {
                var unary = parent as PrefixUnaryExpressionSyntax;
                if (parent.Kind.Equals(SyntaxKind.LogicalNotExpression))
                {
                    // TODO ... or has an operator overload defined...
                    return semModel.Compilation.GetSpecialType(
                        Roslyn.Compilers.SpecialType.System_Boolean);
                }
            }
            else if (parent is IfStatementSyntax || parent is WhileStatementSyntax)
            {
                return semModel.Compilation.GetSpecialType(
                        Roslyn.Compilers.SpecialType.System_Boolean);
            }
            else if (parent is ConditionalExpressionSyntax)
            {
                return (parent as ConditionalExpressionSyntax).GetRetType(semModel);
            }
            else
            {
                Logging.Logger.PrintLine("GetType: parent is " + parent.GetType());
            }
            return null;
        }

        internal static TypeTerm GetRetTypeTerm(this ExpressionSyntax synNode,
                SemanticModel semModel, AbstractTypeInferencer absTypes)
        {
            var parent = synNode.Parent;
            if (parent is EqualsValueClauseSyntax
                && parent.Parent is VariableDeclaratorSyntax
                && parent.Parent.Parent is VariableDeclarationSyntax)
            {
                var decl = parent.Parent.Parent as VariableDeclarationSyntax;
                return absTypes.GetTypeTermFor(semModel.GetSymbolInfo(decl.Type).Symbol);
            }
            else if (parent is ReturnStatementSyntax)
            {
                // TODO yield?
                var ret = parent as ReturnStatementSyntax;
                var containingMethod = ret.FirstAncestorOrSelf<SyntaxNode>(node =>
                    node is MethodDeclarationSyntax) as MethodDeclarationSyntax;
                return absTypes.GetTypeTermFor(semModel.GetDeclaredSymbol(containingMethod));
            }
            else if (parent is BinaryExpressionSyntax
                && parent.Kind.Equals(SyntaxKind.AssignExpression))
            {
                var bin = parent as BinaryExpressionSyntax;
                ExpressionSyntax other;
                if (bin.Left.Equals(synNode))
                {
                    other = bin.Right;
                }
                else
                {
                    other = bin.Left;
                }
                return absTypes.GetTypeTermFor(other, semModel);
            }
            else
            {
                // TODO Do the right matter?
                return null;
            }
        }


    }

    enum InvocationExpressionKind
    {
        UNKNOWN, SEMIKNOWN, KNOWN, ERROR,
        UNKNOWN_STATIC, SEMIKNOWN_STATIC, KNOWN_STATIC
    }
    static class InvocationExpressionKindUtil
    {
        public static InvocationExpressionKind GetInvocationExpressionKind(
                this InvocationExpressionSyntax call, SemanticModel semModel)
        {
            string ignore;
            return call.GetInvocationExpressionKind(semModel, out ignore);
        }
        public static InvocationExpressionKind GetInvocationExpressionKind(
                this InvocationExpressionSyntax call, SemanticModel semModel,
                out string withUnknown)
        {
            var method = call.Expression;
            var methodType = semModel.GetTypeInfo(method).Type;
            var methodSymInfo = semModel.GetSymbolInfo(method);
            var methodSymbol = methodSymInfo.Symbol;

            ExpressionSyntax parent = null;
            SymbolInfo? parentInfo = null;
            var methodAccess = method as MemberAccessExpressionSyntax;
            if (methodAccess != null)
            {
                parent = methodAccess.Expression;
                parentInfo = semModel.GetSymbolInfo(parent);
            }

            if (methodType is ErrorTypeSymbol || (methodSymbol == null && methodSymInfo.CandidateSymbols.IsNullOrEmpty))
            {
                if (method.GetText().Equals("mUnknown") || method.GetText().Equals("m"))
                {
                    withUnknown = "mUnknown";
                    return InvocationExpressionKind.UNKNOWN;
                }
                if (methodAccess != null && methodAccess.Name != null
                    && (methodAccess.Name.GetText().Equals("mUnknown")
                        || methodAccess.Name.GetText().Equals("m")))
                {
                    if (parent != null)
                    {
                        string parentWithUnknown;
                        parent.RewritePartial(semModel, out parentWithUnknown);
                        withUnknown = parentWithUnknown + ".mUnknown";
                        if (parentInfo.Value.Symbol.ResolveAlias() is TypeSymbol)
                        {
                            return InvocationExpressionKind.UNKNOWN_STATIC;
                        }
                        else
                        {
                            return InvocationExpressionKind.UNKNOWN;
                        }
                    }
                }
                withUnknown = method.GetText();
                return InvocationExpressionKind.ERROR;
            }
            else if (methodType == null)
            {
                // method is known but incomplete
                withUnknown = method.GetText();
                if (parentInfo != null && parentInfo.Value.Symbol is TypeSymbol)
                {
                    return InvocationExpressionKind.SEMIKNOWN_STATIC;
                }
                else
                {
                    return InvocationExpressionKind.SEMIKNOWN;
                }
            }
            else if (methodType.TypeKind.Equals(TypeKind.Delegate))
            {
                withUnknown = method.GetText();
                if (parentInfo != null && parentInfo.Value.Symbol is TypeSymbol)
                {
                    return InvocationExpressionKind.KNOWN_STATIC;
                }
                else
                {
                    return InvocationExpressionKind.KNOWN;
                }
            }
            else
            {
                // TODO ?
                withUnknown = method.GetText();
                return InvocationExpressionKind.ERROR;
            }
            //throw new Exception("? Failed to discover invocation expression kind.");
        }
    }
}
